/* 2013/8/18 */
package com.cosmos.util;

import java.util.Date;

/**
 * Represents time span between two date.
 * This class can be used to represent time span between two instances of Date class,
 * so the calculations of Date such as "add", "subtract" are implemented in this class. 
 * @author Leonardo
 * @version 1.1
 */
public class TimeSpan {
	
	/**
	 * Field number for get and set the millisecond of the time.
	 */
	public static final int MILLISECOND = 0;
	
	/**
	 * Field number for get and set the second of the time.
	 */
	public static final int SECOND = 1;
	
	/**
	 * Field number for get and set the minute of the time.
	 */
	public static final int MINUTE = 2;
	
	/**
	 * Field number for get and set the hour of the time.
	 */
	public static final int HOUR = 3;
	
	/**
	 * Field number for get and set the day of the time.
	 */
	public static final int DAY = 4;
	
	/**
	 * The number of distinct fields recognized by get and set.
	 */
	public static final int FIELD_COUNT = 5;
	
	/**
	 * Maximum value of every field.
	 */
	protected static final long[] FIELD_MAX_VALUE = {1000, 60, 60, 24, Long.MAX_VALUE};
	
	private static final long[] FIELD_MILLISECONDS;
	
	/**
	 * Stores the values of the TimeSpan fields.
	 */
	private long timeField[] = new long[FIELD_COUNT];
	
	/**
	 * Represents whether this TimeSpan represents a time point earlier than another.
	 */
	private boolean isEarly = false;
	
	static {
		FIELD_MILLISECONDS = new long[FIELD_COUNT];
		FIELD_MILLISECONDS[MILLISECOND] = 1;
		for (int i = 0; i < FIELD_COUNT - 1; i++) {
			FIELD_MILLISECONDS[i + 1] = FIELD_MILLISECONDS[i] * FIELD_MAX_VALUE[i];  
		}
	}
	
	
	/**
	 * Constructs a TimeSpan object and initializes it so that it represents no time.
	 */
	public TimeSpan() {
		
	}
	
	/**
	 * Constructs a TimeSpan object and initializes it to represent 
	 * the specified number of milliseconds. 
	 * @param time the total milliseconds of the time.
	 */
	public TimeSpan(long time) {
		calculateFields(time);
	}
	
	public static Date add(Date date, TimeSpan timeSpan) {
		return new Date(date.getTime() + timeSpan.getTime());
	}
	
	public static Date subtract(Date date, TimeSpan timeSpan) {
		return new Date(date.getTime() - timeSpan.getTime());
	}
	
	/**
	 * Calculates TimeSpan between two Date.
	 * @param dateA
	 * @param dateB
	 * @return
	 */
	public static TimeSpan subtract(Date dateA, Date dateB) {
		return new TimeSpan(dateA.getTime() - dateB.getTime());
	}	

	
	/**
	 * Compares two TimeSpan for ordering.
	 * @param anotherTime the TimeSpan to be compared.
	 * @return the value 0 if the argument TimeSpan is equal to this TimeSpan;
	 * a value less than 0 if this TimeSpan is smaller than the TimeSpan argument;
	 * and a value greater than 0 if this TimeSpan is bigger than the TimeSpan argument.
	 */
	public int compareTo(TimeSpan anotherTime) {
		return Long.compare(this.getTime(), anotherTime.getTime());
	}
	
	@Override
	/**
	 * Compares two TimeSpan for equality. The result is true if and only if 
	 * the argument is not null and is a TimeSpan object that represents the 
	 * same time span as this object.
	 */
	public boolean equals(Object obj) {
		if (obj != null && obj instanceof TimeSpan) {
			if (((TimeSpan)obj).getTime() == this.getTime()) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Returns the value of the given TimeSpan field.
	 * @param field the given TimeSpan field.
	 * @return the value for the given TimeSpan field.
	 * @exception ArrayIndexOutOfBoundsException if the specified field is out of range
	 * (field < 0 || field >= FIELD_COUNT).
	 */
	public int get(int field) throws ArrayIndexOutOfBoundsException {
		if (field < 0 || field >= FIELD_COUNT) {
			throw new ArrayIndexOutOfBoundsException(field);
		}		
		
		return (int)timeField[field];
	}
	
	/**
	 * Returns the number of milliseconds represented by this TimeSpan object. 
	 * @return the number of milliseconds.
	 */
	public long getTime() {
		return calculateTime();
	}
	
	/**
	 * Determines whether this TimeSpan represents a time point earlier than another.
	 * @return true if this TimeSpan represents a time point earlier than another; false otherwise.
	 */
	public boolean isEarly() {
		return isEarly;
	}
	
	/**
	 * Sets the given TimeSpan field to the given value.
	 * @param field the given TimeSpan field.
	 * @param value the value to be set for the given TimeSpan field.
	 * @throws ArrayIndexOutOfBoundsException if the specified field is out of range
	 * (field < 0 || field >= FIELD_COUNT).
	 */
	public void set (int field, int value) throws ArrayIndexOutOfBoundsException {
		if (field < 0 || field >= FIELD_COUNT) {
			throw new ArrayIndexOutOfBoundsException(field);
		}
		
		if (value < 0 || value >= FIELD_MAX_VALUE[field]) {
			throw new IllegalArgumentException();
		}
		
		timeField[field] = value;
	}
	
	public void setEarly(boolean isEarly) {
		this.isEarly = isEarly;
	}
	
	/**
	 * Sets this TimeSpan object to represent the specified number of milliseconds.
	 * @param time the number of milliseconds.
	 */
	public void setTime(long time) {
		calculateFields(time);
	}
	
	/**
	 * Returns a string representation of this TimeSpan.
	 * @return a string representation of this TimeSpan.
	 */
	@Override
	public String toString() {		
		String result = null;
		if (isEarly()) {
			result = String.format("-%1$d %2$02d:%3$02d:%4$02d.%5$03d",
					timeField[DAY], timeField[HOUR], timeField[MINUTE], timeField[SECOND], timeField[MILLISECOND]);
		} else {
			result = String.format("%1$d %2$02d:%3$02d:%4$02d.%5$03d",
					timeField[DAY], timeField[HOUR], timeField[MINUTE], timeField[SECOND], timeField[MILLISECOND]);
		}
		
		return result;
	}

	private long calculateTime() {
		long timeSpan = timeField[DAY];
		for (int i = FIELD_COUNT - 2; i >= 0; i--) {
			timeSpan *= FIELD_MAX_VALUE[i];
			timeSpan += timeField[i];
		}
		
		if (isEarly()) {
			timeSpan = -timeSpan;
		}
		
		return timeSpan;
	}
	
	private long calculateTimeByArray() {
		long timeSpan = 0;
		for (int i = 0; i < FIELD_COUNT; i++) {
			timeSpan += timeField[i] * FIELD_MILLISECONDS[i];
		}
		
		if (isEarly) {
			timeSpan = -timeSpan;
		}
		
		return timeSpan;
	}

	private void calculateFields(long time) {
		if (time < 0) {
			isEarly = true;
			timeField[MILLISECOND] = -time;
		} else {
			timeField[MILLISECOND] = time;
		}
		
		for (int i = 0; i < FIELD_COUNT; i++) {
			if (timeField[i] >= FIELD_MAX_VALUE[i]) {
				long temp = timeField[i];
				timeField[i + 1] = temp / FIELD_MAX_VALUE[i];
				timeField[i] = temp % FIELD_MAX_VALUE[i];
			}
		}
	}
	
	private void calculateFieldsByArray(long time) {
		for (int i = FIELD_COUNT - 1; i >= 0; i--) {
			if (time >= FIELD_MILLISECONDS[i]) {
				timeField[i] = time / FIELD_MILLISECONDS[i];
				time = time - timeField[i] * FIELD_MILLISECONDS[i];
			}
		}
	}
}
